Customer mindset metrics

These Python notes build on the conceptual knowledge of customer attitude metrics. We encourage the reader to review the case study provided in Chapter 6 of the book before starting the analysis below.

By the end of the application, you will be able to:

  • Make evidence-based marketing mix decisions by examining the diagnostic value that customer mindset metrics provide

  • Quantify the short- and long-run gains in sales that originate from improvement in mindset metrics.

Preparation and Set Up

Before we start, let us do some preparation work to get ready.

We need to:

  • Create a folder (e.g. mindset or chapter 6) and set the working directory to that folder on your device.

  • Download the data set needed for this Python application onto your working directory. The data set for this application, mindset_shampoo.csv is available on the website of this book.

  • Load the data set to Jupyter Notebbok for analysis as we proceed.

In order to load the data sets to Jupyter Notebbok, we need to specify the working directory where the dataset for this tutorial is located.

In [97]:
#Find the current working directory 
import os
cwd = os.getcwd()
print(cwd) 

#Change the working directory 
os.chdir('C:/Users/gyildiri/Downloads/chapter 6')
print(os.getcwd())
C:\Users\gyildiri\Downloads\chapter 6
C:\Users\gyildiri\Downloads\chapter 6

Now we are ready to go.

1. Data

In [98]:
# Install the libraries
%pip install pandas statsmodels matplotlib seaborn pmdarima
Requirement already satisfied: pandas in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (2.1.4)
Requirement already satisfied: statsmodels in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (0.14.0)
Requirement already satisfied: matplotlib in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (3.8.0)
Requirement already satisfied: seaborn in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (0.12.2)
Requirement already satisfied: pmdarima in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (2.0.4)
Requirement already satisfied: numpy<2,>=1.23.2 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from pandas) (1.26.4)
Requirement already satisfied: python-dateutil>=2.8.2 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from pandas) (2.8.2)
Requirement already satisfied: pytz>=2020.1 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from pandas) (2023.3.post1)
Requirement already satisfied: tzdata>=2022.1 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from pandas) (2023.3)
Requirement already satisfied: scipy!=1.9.2,>=1.4 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from statsmodels) (1.11.4)
Requirement already satisfied: patsy>=0.5.2 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from statsmodels) (0.5.3)
Requirement already satisfied: packaging>=21.3 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from statsmodels) (23.1)
Requirement already satisfied: contourpy>=1.0.1 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from matplotlib) (1.2.0)
Requirement already satisfied: cycler>=0.10 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from matplotlib) (0.11.0)
Requirement already satisfied: fonttools>=4.22.0 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from matplotlib) (4.25.0)
Requirement already satisfied: kiwisolver>=1.0.1 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from matplotlib) (1.4.4)
Requirement already satisfied: pillow>=6.2.0 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from matplotlib) (10.2.0)
Requirement already satisfied: pyparsing>=2.3.1 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from matplotlib) (3.0.9)
Requirement already satisfied: joblib>=0.11 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from pmdarima) (1.2.0)
Requirement already satisfied: Cython!=0.29.18,!=0.29.31,>=0.29 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from pmdarima) (3.0.10)
Requirement already satisfied: scikit-learn>=0.22 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from pmdarima) (1.2.2)
Requirement already satisfied: urllib3 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from pmdarima) (2.0.7)
Requirement already satisfied: setuptools!=50.0.0,>=38.6.0 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from pmdarima) (68.2.2)
Requirement already satisfied: six in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from patsy>=0.5.2->statsmodels) (1.16.0)
Requirement already satisfied: threadpoolctl>=2.0.0 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from scikit-learn>=0.22->pmdarima) (2.2.0)
Note: you may need to restart the kernel to use updated packages.
In [99]:
import pandas as pd
import statsmodels.api as sm
import numpy as np
import statsmodels.tsa.vector_ar as vecm
import matplotlib.pyplot as plt
import seaborn as sns
from statsmodels.tsa.ar_model import AutoReg, ar_select_order
from pmdarima import auto_arima
In [100]:
file_path = 'mindset_shampoo.csv'

# Read the CSV file into a DataFrame
df = pd.read_csv(file_path)

# Display the DataFrame 
print(df.head())
   time  consideration  liking  awareness  promotion  advertising  price  \
0     1      14.260000   5.136  26.780001       2.39   449.571991  4.349   
1     2      16.770000   5.010  29.930000       4.90   483.415985  4.406   
2     3      17.170000   4.901  29.629999       3.80   374.109985  4.203   
3     4      16.459999   5.094  27.110001       2.94   567.109985  4.145   
4     5      18.430000   4.944  31.770000       2.81   418.777008  4.212   

           vol  
0  1280.439973  
1  1129.629970  
2  1059.380054  
3   933.610022  
4   826.359987  

Next, we will calculate our four criteria: potential, stickiness, and responsiveness, and conversion. Finally, by aggregating the results from all these stages we will evaluate the appeal of our marketing mix decisions.

2. Four criteria

Potential

For awareness and consideration, we need to calculate the average level of awareness and consideration for this shampoo brand and then calculate the potential by deducting one from them.

For liking, things are slightly more complicated since those values are measured on a seven-point scale. Therefore, we need to first convert those values into percentages (e.g. if liking is 3.5 out of 7, then it is actually 50 per cent of the full score and the value should be 50) and then take the average and calculate the corresponding potential.

Let’s reach the following using Python:

In [101]:
# Calculate potential for awareness and consideration
potential_awareness = (100 - df['awareness'].mean()) / 100
potential_consideration = (100 - df['consideration'].mean()) / 100

# Calculate potential for liking
df['liking_transformed'] = df['liking'] / 7
potential_liking = 1 - df['liking_transformed'].mean()

table_potential = pd.DataFrame({
    'potential': ['awareness', 'consideration', 'liking'],
    'value': [potential_awareness, potential_consideration, potential_liking]
})

print(table_potential)
       potential     value
0      awareness  0.725270
1  consideration  0.825208
2         liking  0.263096

We get that potential is 0.725 for awareness, 0.825 for consideration and 0.263 for liking.

Question: Judging from what you have now, which mindset metric should you put more effort into? Why?

Stickiness

Stickiness, or inertia, is the degree to which a change in the level of a metric is sustained over time, without any new stimuli. This can be modelled by a simple univariate AR(p) process on the attitude metric, where stickiness is quantified as the sum of the autoregressive coefficients. For further details, please refer to Chapter 6 of the book.

To compute stickiness, we need to run the following code in Python. It is important to note that the order of lag for these AR models is determined by model fit statistics, such as AIC or BIC.

In [102]:
# awareness
ar1_order = auto_arima(df['awareness'], max_order=3, information_criterion='aic')
ar1_lags = ar1_order.order[0]

# consideration
ar2_order = auto_arima(df['consideration'], max_order=3, information_criterion='aic')
ar2_lags = ar2_order.order[0]

# liking
ar3_order = auto_arima(df['liking'], max_order=3, information_criterion='aic')
ar3_lags = ar3_order.order[0]

# AutoReg modeling
result_ar1 = AutoReg(df['awareness'], lags=3).fit()
print(ar1_lags)
print(result_ar1.summary())

result_ar2 = AutoReg(df['consideration'], lags=3).fit()
print(ar2_lags)
print(result_ar2.summary())

result_ar3 = AutoReg(df['liking'], lags=3).fit()
print(result_ar3.summary())
0
                            AutoReg Model Results                             
==============================================================================
Dep. Variable:              awareness   No. Observations:                   96
Model:                     AutoReg(3)   Log Likelihood                -230.957
Method:               Conditional MLE   S.D. of innovations              2.899
Date:                Fri, 10 May 2024   AIC                            471.915
Time:                        16:05:26   BIC                            484.578
Sample:                             3   HQIC                           477.027
                                   96                                         
================================================================================
                   coef    std err          z      P>|z|      [0.025      0.975]
--------------------------------------------------------------------------------
const            6.3947      2.548      2.510      0.012       1.401      11.389
awareness.L1     0.4521      0.100      4.528      0.000       0.256       0.648
awareness.L2     0.0458      0.110      0.417      0.677      -0.170       0.261
awareness.L3     0.2689      0.100      2.701      0.007       0.074       0.464
                                    Roots                                    
=============================================================================
                  Real          Imaginary           Modulus         Frequency
-----------------------------------------------------------------------------
AR.1            1.1565           -0.0000j            1.1565           -0.0000
AR.2           -0.6635           -1.6660j            1.7933           -0.3103
AR.3           -0.6635           +1.6660j            1.7933            0.3103
-----------------------------------------------------------------------------
0
                            AutoReg Model Results                             
==============================================================================
Dep. Variable:          consideration   No. Observations:                   96
Model:                     AutoReg(3)   Log Likelihood                -209.518
Method:               Conditional MLE   S.D. of innovations              2.302
Date:                Fri, 10 May 2024   AIC                            429.036
Time:                        16:05:26   BIC                            441.699
Sample:                             3   HQIC                           434.149
                                   96                                         
====================================================================================
                       coef    std err          z      P>|z|      [0.025      0.975]
------------------------------------------------------------------------------------
const                3.7217      1.612      2.308      0.021       0.562       6.882
consideration.L1     0.3091      0.104      2.984      0.003       0.106       0.512
consideration.L2     0.3199      0.104      3.075      0.002       0.116       0.524
consideration.L3     0.1620      0.103      1.568      0.117      -0.040       0.364
                                    Roots                                    
=============================================================================
                  Real          Imaginary           Modulus         Frequency
-----------------------------------------------------------------------------
AR.1            1.1351           -0.0000j            1.1351           -0.0000
AR.2           -1.5553           -1.7380j            2.3322           -0.3662
AR.3           -1.5553           +1.7380j            2.3322            0.3662
-----------------------------------------------------------------------------
                            AutoReg Model Results                             
==============================================================================
Dep. Variable:                 liking   No. Observations:                   96
Model:                     AutoReg(3)   Log Likelihood                  26.938
Method:               Conditional MLE   S.D. of innovations              0.181
Date:                Fri, 10 May 2024   AIC                            -43.877
Time:                        16:05:26   BIC                            -31.214
Sample:                             3   HQIC                           -38.764
                                   96                                         
==============================================================================
                 coef    std err          z      P>|z|      [0.025      0.975]
------------------------------------------------------------------------------
const          1.2341      0.471      2.619      0.009       0.310       2.158
liking.L1      0.4193      0.100      4.173      0.000       0.222       0.616
liking.L2      0.1165      0.109      1.070      0.285      -0.097       0.330
liking.L3      0.2257      0.100      2.250      0.024       0.029       0.422
                                    Roots                                    
=============================================================================
                  Real          Imaginary           Modulus         Frequency
-----------------------------------------------------------------------------
AR.1            1.1628           -0.0000j            1.1628           -0.0000
AR.2           -0.8395           -1.7621j            1.9519           -0.3208
AR.3           -0.8395           +1.7621j            1.9519            0.3208
-----------------------------------------------------------------------------

From the results above, we can calculate the stickiness of our mindset metrics by summing their AR coefficients as follows.

In [103]:
stick_awareness = result_ar1.params['awareness.L1'] + result_ar1.params['awareness.L2'] + result_ar1.params['awareness.L3']
stick_consideration = result_ar2.params['consideration.L1'] + result_ar2.params['consideration.L2'] + result_ar2.params['consideration.L3']
stick_liking = result_ar3.params['liking.L1'] + result_ar3.params['liking.L2'] + result_ar3.params['liking.L3']
print(stick_awareness.round(3), stick_consideration.round(3), stick_liking.round(3))
0.767 0.791 0.761

To summarize, the stickiness measures for the awareness, consideration, and liking metrics are 0.767, 0.791 and 0.761, respectively.

Responsiveness

Responsiveness, or lift, is the short-term response of the attitude metric with respect to a marketing stimulus. We recommend using well-established, robust response functions to estimate responsiveness. For example, the standard multiplicative response model produces elasticities as the following responsiveness metrics:

$$ Y_t= cY_{t-1}^\gamma X_{1t}^{\beta_1} X_{2t}^{\beta_2} e^{u_t} $$

where Y is an attitude metric and $X_i$ (i=1,2) are marketing instruments. Not only do such response models provide readily interpretable results, but they have also been shown to outperform more complex specifications in forecasting product trials for consumer-packaged goods.

Note that responsiveness may be related to potential as follows: the closer the attitude metric is to its ceiling value, the more difficult it will be to register further increases through marketing. To reflect that in the calculation, once we get the responsiveness of each mindset metric and sales, we can multiply it with the corresponding potential:

$$ \theta_i = \beta_i * Potential_i $$

To estimate the responsiveness model, which is multiplicative, an easy way is to take the logarithm of both sides of the equation and estimate the model as a log-linear one. To be specific, we first take the log such that:

$$ log(Y_t)= c'+\gamma log(Y_{t-1}) + {\beta_1} log(X_{1t}) + {\beta_2} log(X_{2t}) + {u_t} $$

Now, we examine the log-linear model for each mindset metric and sales to get responsiveness:

In [104]:
#Define the lagged Variables
df['lag_aware'] = df['awareness'].shift(1)
df.loc[0, 'lag_aware'] = 0

df['lag_liking'] = df['liking'].shift(1)
df.loc[0, 'lag_liking'] = 0

df['lag_consideration'] = df['consideration'].shift(1)
df.loc[0, 'lag_consideration'] = 0

df['lag_sales'] = df['vol'].shift(1)
df.loc[0, 'lag_sales'] = 0

# Define the log-linear models
response_aware = sm.OLS(np.log1p(df['awareness']), sm.add_constant(np.log1p(df[['lag_aware', 'price', 'promotion', 'advertising']])))
result_aware = response_aware.fit()

response_consideration = sm.OLS(np.log1p(df['consideration']), sm.add_constant(np.log1p(df[['lag_consideration', 'price', 'promotion', 'advertising']])))
result_consideration = response_consideration.fit()

response_liking = sm.OLS(np.log1p(df['liking']), sm.add_constant(np.log1p(df[['lag_liking', 'price', 'promotion', 'advertising']])))
result_liking = response_liking.fit()

response_sales = sm.OLS(np.log1p(df['vol']), sm.add_constant(np.log1p(df[['lag_sales', 'price', 'promotion', 'advertising']])))
result_sales = response_sales.fit()

# Summarize the regression results
print(result_aware.summary())
print(result_consideration.summary())
print(result_liking.summary())
print(result_sales.summary())
                            OLS Regression Results                            
==============================================================================
Dep. Variable:              awareness   R-squared:                       0.427
Model:                            OLS   Adj. R-squared:                  0.402
Method:                 Least Squares   F-statistic:                     16.97
Date:                Fri, 10 May 2024   Prob (F-statistic):           1.98e-10
Time:                        16:05:31   Log-Likelihood:                 77.882
No. Observations:                  96   AIC:                            -145.8
Df Residuals:                      91   BIC:                            -132.9
Df Model:                           4                                         
Covariance Type:            nonrobust                                         
===============================================================================
                  coef    std err          t      P>|t|      [0.025      0.975]
-------------------------------------------------------------------------------
const           2.9509      0.554      5.331      0.000       1.851       4.051
lag_aware       0.0467      0.032      1.446      0.152      -0.017       0.111
price          -0.0830      0.331     -0.250      0.803      -0.741       0.575
promotion       0.0215      0.015      1.458      0.148      -0.008       0.051
advertising     0.0521      0.008      6.729      0.000       0.037       0.067
==============================================================================
Omnibus:                       13.507   Durbin-Watson:                   1.448
Prob(Omnibus):                  0.001   Jarque-Bera (JB):               14.501
Skew:                          -0.883   Prob(JB):                     0.000710
Kurtosis:                       3.710   Cond. No.                         444.
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
                            OLS Regression Results                            
==============================================================================
Dep. Variable:          consideration   R-squared:                       0.341
Model:                            OLS   Adj. R-squared:                  0.312
Method:                 Least Squares   F-statistic:                     11.79
Date:                Fri, 10 May 2024   Prob (F-statistic):           9.33e-08
Time:                        16:05:31   Log-Likelihood:                 59.041
No. Observations:                  96   AIC:                            -108.1
Df Residuals:                      91   BIC:                            -95.26
Df Model:                           4                                         
Covariance Type:            nonrobust                                         
=====================================================================================
                        coef    std err          t      P>|t|      [0.025      0.975]
-------------------------------------------------------------------------------------
const                 1.8628      0.654      2.846      0.005       0.563       3.163
lag_consideration     0.0996      0.044      2.253      0.027       0.012       0.187
price                 0.2801      0.401      0.698      0.487      -0.517       1.077
promotion             0.0662      0.018      3.622      0.000       0.030       0.102
advertising           0.0177      0.009      1.883      0.063      -0.001       0.036
==============================================================================
Omnibus:                        1.577   Durbin-Watson:                   1.409
Prob(Omnibus):                  0.454   Jarque-Bera (JB):                1.071
Skew:                          -0.229   Prob(JB):                        0.585
Kurtosis:                       3.239   Cond. No.                         425.
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
                            OLS Regression Results                            
==============================================================================
Dep. Variable:                 liking   R-squared:                       0.329
Model:                            OLS   Adj. R-squared:                  0.300
Method:                 Least Squares   F-statistic:                     11.16
Date:                Fri, 10 May 2024   Prob (F-statistic):           2.08e-07
Time:                        16:05:31   Log-Likelihood:                 195.26
No. Observations:                  96   AIC:                            -380.5
Df Residuals:                      91   BIC:                            -367.7
Df Model:                           4                                         
Covariance Type:            nonrobust                                         
===============================================================================
                  coef    std err          t      P>|t|      [0.025      0.975]
-------------------------------------------------------------------------------
const           1.4541      0.161      9.037      0.000       1.134       1.774
lag_liking     -0.0016      0.018     -0.089      0.929      -0.038       0.035
price           0.1789      0.097      1.843      0.069      -0.014       0.372
promotion       0.0192      0.004      4.434      0.000       0.011       0.028
advertising     0.0024      0.002      1.053      0.295      -0.002       0.007
==============================================================================
Omnibus:                        3.893   Durbin-Watson:                   1.317
Prob(Omnibus):                  0.143   Jarque-Bera (JB):                2.387
Skew:                           0.167   Prob(JB):                        0.303
Kurtosis:                       2.303   Cond. No.                         411.
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
                            OLS Regression Results                            
==============================================================================
Dep. Variable:                    vol   R-squared:                       0.590
Model:                            OLS   Adj. R-squared:                  0.572
Method:                 Least Squares   F-statistic:                     32.73
Date:                Fri, 10 May 2024   Prob (F-statistic):           6.73e-17
Time:                        16:05:31   Log-Likelihood:                -13.027
No. Observations:                  96   AIC:                             36.05
Df Residuals:                      91   BIC:                             48.88
Df Model:                           4                                         
Covariance Type:            nonrobust                                         
===============================================================================
                  coef    std err          t      P>|t|      [0.025      0.975]
-------------------------------------------------------------------------------
const           6.2350      1.411      4.420      0.000       3.433       9.037
lag_sales       0.0642      0.039      1.647      0.103      -0.013       0.142
price          -0.2776      0.852     -0.326      0.745      -1.969       1.414
promotion       0.2970      0.041      7.214      0.000       0.215       0.379
advertising     0.0596      0.020      2.989      0.004       0.020       0.099
==============================================================================
Omnibus:                        2.721   Durbin-Watson:                   0.906
Prob(Omnibus):                  0.256   Jarque-Bera (JB):                2.381
Skew:                          -0.135   Prob(JB):                        0.304
Kurtosis:                       3.723   Cond. No.                         574.
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

Then, we summarize our estimation results in the below table:

In [105]:
# Create tables of rounded coefficients
table_response_aware = pd.DataFrame(round(result_aware.params, 4), columns=['Estimate'])
table_response_consideration = pd.DataFrame(round(result_consideration.params, 4), columns=['Estimate'])
table_response_liking = pd.DataFrame(round(result_liking.params, 4), columns=['Estimate'])
table_response_sales = pd.DataFrame(round(result_sales.params, 4), columns=['Estimate'])

# Display the tables
print("Table Response Aware:")
print(table_response_aware)

print("\nTable Response Consideration:")
print(table_response_consideration)

print("\nTable Response Consideration:")
print(table_response_consideration)

print("\nTable Response Liking:")
print(table_response_liking)

print("\nTable Response Sales:")
print(table_response_sales)
Table Response Aware:
             Estimate
const          2.9509
lag_aware      0.0467
price         -0.0830
promotion      0.0215
advertising    0.0521

Table Response Consideration:
                   Estimate
const                1.8628
lag_consideration    0.0996
price                0.2801
promotion            0.0662
advertising          0.0177

Table Response Consideration:
                   Estimate
const                1.8628
lag_consideration    0.0996
price                0.2801
promotion            0.0662
advertising          0.0177

Table Response Liking:
             Estimate
const          1.4541
lag_liking    -0.0016
price          0.1789
promotion      0.0192
advertising    0.0024

Table Response Sales:
             Estimate
const          6.2350
lag_sales      0.0642
price         -0.2776
promotion      0.2970
advertising    0.0596

Conversion

An attitudinal metric would be meaningless for a brand if it does not impact sales. That is why we need to evaluate its power in terms of changing sales. Conversion is the degree to which movements in the attitudinal metric convert to sales, similar to a conversion rate of leads into customer orders in B2B. To calculate conversion, we again allow for a multiplicative funnel model that can be applied across different conditions.

For example, with intermediate attitudinal metrics awareness ($A_t$), consideration ($C_t$ ) and liking ($L_t$), a multiplicative funnel model for sales revenue ($S_t$) would be:

$$ S_t= cS_{t-1}^\gamma A_{t}^{\beta_1} C_{t}^{\beta_2} L_{t}^{\beta_3} e^{v_t} $$

To estimate the conversion model, we again take the log of both sides of the equation above and estimate a linear model.

In [106]:
# Define the log-linear model formula using np.log for awareness, consideration, and liking
formula_conversion = 'np.log1p(vol) ~ np.log1p(lag_sales) + np.log(awareness) + np.log(consideration) + np.log(liking)'

# Fit the OLS model with a constant term
conversion_model = sm.OLS.from_formula(formula=formula_conversion, data=df)
result_conversion = conversion_model.fit()

# Summarize the regression results
print(result_conversion.summary())
                            OLS Regression Results                            
==============================================================================
Dep. Variable:          np.log1p(vol)   R-squared:                       0.562
Model:                            OLS   Adj. R-squared:                  0.542
Method:                 Least Squares   F-statistic:                     29.14
Date:                Fri, 10 May 2024   Prob (F-statistic):           1.35e-15
Time:                        16:05:45   Log-Likelihood:                -16.241
No. Observations:                  96   AIC:                             42.48
Df Residuals:                      91   BIC:                             55.30
Df Model:                           4                                         
Covariance Type:            nonrobust                                         
=========================================================================================
                            coef    std err          t      P>|t|      [0.025      0.975]
-----------------------------------------------------------------------------------------
Intercept                -1.8907      1.246     -1.517      0.133      -4.366       0.585
np.log1p(lag_sales)       0.1121      0.038      2.925      0.004       0.036       0.188
np.log(awareness)         0.8010      0.253      3.167      0.002       0.299       1.303
np.log(consideration)     0.4267      0.304      1.405      0.163      -0.177       1.030
np.log(liking)            2.8141      0.997      2.821      0.006       0.833       4.795
==============================================================================
Omnibus:                        0.035   Durbin-Watson:                   1.458
Prob(Omnibus):                  0.983   Jarque-Bera (JB):                0.155
Skew:                           0.037   Prob(JB):                        0.926
Kurtosis:                       2.817   Cond. No.                         450.
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

Summarizing the conversion results, we obtain:

In [107]:
coefficients = result_conversion.params

table_conversion = pd.DataFrame({'Estimate': coefficients.values}, index=coefficients.index)
table_conversion = table_conversion.round(4)
print(table_conversion)
                       Estimate
Intercept               -1.8907
np.log1p(lag_sales)      0.1121
np.log(awareness)        0.8010
np.log(consideration)    0.4267
np.log(liking)           2.8141

Summary of the results

The following code combines all the results from previous states together to gain a clear understanding of the measures on potential, stickiness, responsiveness, and conversion.

In [109]:
import warnings
warnings.filterwarnings("ignore")

mean_awareness = np.mean(df['awareness']) / 100
mean_consideration = np.mean(df['consideration']) / 100
mean_liking = np.mean(df['liking_transformed']) / 100

# Calculate potential for awareness and consideration
potential_awareness = round((1 - mean_awareness), 3)
potential_consideration = round((1 - mean_consideration), 3)

# Calculate potential for liking
df['liking_transformed'] = df['liking'] / 7
potential_liking = round((1 - df['liking_transformed'].mean()), 3)

# Create DataFrame
table_final = pd.DataFrame({
    'Item': ["beginning level", "potential", "stickiness", "responsiveness to advertising",
             "responsiveness to promotion", "conversion"],
    'awareness': [
        round(mean_awareness, 3),
        potential_awareness,
        round(stick_awareness, 3),
        round(result_aware.params[4], 3),
        round(result_aware.params[3], 3),
        round(result_conversion.params[2], 3)
    ],
    'consideration': [
        round(mean_consideration, 3),
        potential_consideration,
        round(stick_consideration, 3),
        round(result_consideration.params[4], 3),
        round(result_consideration.params[3], 3),
        round(result_conversion.params[3], 3)
    ],
    'liking': [
        round(mean_liking, 3),
        potential_liking,
        round(stick_liking, 3),
        round(result_liking.params[4], 3),
        round(result_liking.params[3], 3),
        round(result_conversion.params[4], 3)
    ]
})

# Display the final table
print(table_final)
                            Item  awareness  consideration  liking
0                beginning level      0.275          0.175   0.007
1                      potential      0.725          0.825   0.263
2                     stickiness      0.767          0.791   0.761
3  responsiveness to advertising      0.052          0.018   0.002
4    responsiveness to promotion      0.021          0.066   0.019
5                     conversion      0.801          0.427   2.814

3. Appeal

Finally, the appeal of each marketing mix action (i.e. promotion and advertising) is calculated by multiplying the potential, responsiveness, long-run multiplier that uses stickiness,and conversion.

$$ Appeal = Potential * Reponsiveness_{short-run}* 1/(1-Stickiness) * Conversion $$
In [110]:
# Assuming 'table_final', 'result_advertising', 'result_promotion', 'result_conversion' are defined

# Calculate appeal for awareness - advertising
appeal_adv_awareness = round(
    table_final.loc[1, 'awareness'] *  # potential
    table_final.loc[3, 'awareness'] *  # responsiveness
    (1 / (1 - table_final.loc[2, 'awareness'])) *  # long-run multiplier using stickiness
    table_final.loc[5, 'awareness'],  # conversion
    3
)

# Calculate appeal for awareness - promotion
appeal_promo_awareness = round(
    table_final.loc[1, 'awareness'] *  # potential
    table_final.loc[4, 'awareness'] *  # responsiveness
    (1 / (1 - table_final.loc[2, 'awareness'])) *  # long-run multiplier using stickiness
    table_final.loc[5, 'awareness'],  # conversion
    3
)

# Calculate appeal for consideration - advertising
appeal_adv_consideration = round(
    table_final.loc[1, 'consideration'] *  # potential
    table_final.loc[3, 'consideration'] *  # responsiveness
    (1 / (1 - table_final.loc[2, 'consideration'])) *  # long-run multiplier using stickiness
    table_final.loc[5, 'consideration'],  # conversion
    3
)

# Calculate appeal for consideration - promotion
appeal_promo_consideration = round(
    table_final.loc[1, 'consideration'] *  # potential
    table_final.loc[4, 'consideration'] *  # responsiveness
    (1 / (1 - table_final.loc[2, 'consideration'])) *  # long-run multiplier using stickiness
    table_final.loc[5, 'consideration'],  # conversion
    3
)

# Calculate appeal for liking - advertising
appeal_adv_liking = round(
    table_final.loc[1, 'liking'] *  # potential
    table_final.loc[3, 'liking'] *  # responsiveness
    (1 / (1 - table_final.loc[2, 'liking'])) *  # long-run multiplier using stickiness
    table_final.loc[5, 'liking'],  # conversion
    3
)

# Calculate appeal for liking - promotion
appeal_promo_liking = round(
    table_final.loc[1, 'liking'] *  # potential
    table_final.loc[4, 'liking'] *  # responsiveness
    (1 / (1 - table_final.loc[2, 'liking'])) *  # long-run multiplier using stickiness
    table_final.loc[5, 'liking'],  # conversion
    3
)

# Create DataFrame for appeal
appeal_table = pd.DataFrame({
    'Item': ["appeal_advertising", "appeal_promotion"],
    'awareness': [appeal_adv_awareness, appeal_promo_awareness],
    'consideration': [appeal_adv_consideration, appeal_promo_consideration],
    'liking': [appeal_adv_liking, appeal_promo_liking]
})

# Display the appeal table
print(appeal_table)
                 Item  awareness  consideration  liking
0  appeal_advertising      0.130          0.030   0.006
1    appeal_promotion      0.052          0.111   0.059

Question: What is your takeaway from these results? Which marketing tool (i.e. promotion or advertising) is more effective for the marketing manager of Zanten to improve consumer awareness, consideration and liking? If you were the manager of this shampoo brand, what type of marketing strategy would you like to adopt in general?

4. What-if analysis

Given that this chapter focuses on the value of marketing activities through consumer mindset metrics on brand sales, we would like to evaluate the impact of change in a particular marketing tool on long-term sales and decompose such effect into gain/loss due to mindset metrics and gain/loss due to transactions.

In particular, we will evaluate two alternative options for Zhang, the marketing manager of Zanten:

  1. Quintupling advertising spending

  2. Doubling promotion

Quintupling advertising spending

Suppose that Zhang decides to quintuple advertising spending from \£100K to \£500K while keeping the rest of the marketing mix decisions same.

Let us visualise the calculations by using the below table:

In [111]:
mean_awareness = np.mean(df['awareness']) / 100
mean_consideration = np.mean(df['consideration']) / 100
mean_liking = np.mean(df['liking_transformed'])
mean_sales = np.mean(df['vol'])

# Creating the DataFrame
table_whatif = pd.DataFrame({
    'Item': ["Advertising", "Promotion", "Awareness", "Consideration", "Liking", "Sales"],
    'Start': [100, 100, round(mean_awareness, 3), round(mean_consideration, 3), round(mean_liking, 3), round(mean_sales, 3)],
    'New': [500, 100, "?", "?", "?", "?"],
    'Gain': [400, 0, "?", "?", "?", "?"],
    'LRGain': ["/", "/", "?", "?", "?", "?"],
    'Contribution': ["/", "/", "?", "?", "?", "/"]
})

# Print the DataFrame
print(table_whatif)
            Item     Start  New Gain LRGain Contribution
0    Advertising   100.000  500  400      /            /
1      Promotion   100.000  100    0      /            /
2      Awareness     0.275    ?    ?      ?            ?
3  Consideration     0.175    ?    ?      ?            ?
4         Liking     0.737    ?    ?      ?            ?
5          Sales  1800.391    ?    ?      ?            /

The cells with question marks are the ones that we need to fill in. We proceed as follows:

To briefly summarise, we need to do the following:

  1. First, we derive the new level of each mindset metric and sales based on the changes that we decide to make to the marketing mix decisions (i.e. advertising).

  2. Then, by comparing the new and the old levels, we can derive immediate or short-run gain for each metric and sales.

  3. Next, with the help of carryover parameters from responsiveness models, we transform those short-run gains into corresponding long-run gains.

  4. Finally, to decompose the long-run gain in sales and attribute it to gains due to mindset metrics and gains due to transaction, we calculate how much the long-run gains from step 3 translate into an actual increase in sales.

New values

From the table, the column ‘Start’ summarizes all the base values for each item. For example, the average level of awareness, expressed in percentage terms, is 27.5 per cent, leaving a 72.5 per cent potential.

Now, we need to decide what happens to all the mindset metrics and sales after we quintuple advertising (from 100K to 500K). First, we need to determine the new level of each metric and sale after the change to the marketing mix. More specifically, the change to marketing mix exerts its impact on each metric through short-run responsiveness. Therefore, we need to go back to our previous analysis and use the model parameters in responsiveness models. For mindset metric $M_i$ (i=1,2,3 in our case), we apply the formula below:

$$ New_{M_i} = Start_{M_i} *(Advertising_{new}/Advertising_{old})^{Responsiveness_{Advertising_{M_i}}}) $$
In [112]:
# Calculate new awareness
new_awareness = round((1 - table_potential.iloc[0, 1]) * (table_whatif.loc[0, 'New'] / table_whatif.loc[0, 'Start']) ** table_final.loc[3, 'awareness'], 3)

# Calculate new consideration
new_consideration = round((1 - table_potential.iloc[1, 1]) * (table_whatif.loc[0, 'New'] / table_whatif.loc[0, 'Start']) ** table_final.loc[3, 'consideration'], 3)

# Calculate new liking
new_liking = round((1 - table_potential.iloc[2, 1]) * (table_whatif.loc[0, 'New'] / table_whatif.loc[0, 'Start']) ** table_final.loc[3, 'liking'], 3)

# Calculate new sales
new_sales = round(np.mean(df['vol']) * (table_whatif.loc[0, 'New'] / table_whatif.loc[0, 'Start']) ** float(result_sales.params[4]), 3)

Short-run gain

We derive the gain (loss if negative) in each mindset metric and sales by simply calculating the growth rate. The code below calculates the short-run gains.

In [113]:
# Calculate gain (from "start" to "new")
gain_awareness = round((new_awareness / (1 - table_potential.iloc[0, 1])) - 1, 3)
gain_consideration = round((new_consideration / (1 - table_potential.iloc[1, 1])) - 1, 3)
gain_liking = round((new_liking / (1 - table_potential.iloc[2, 1])) - 1, 3)
gain_sales = round((new_sales / np.mean(df['vol'])) - 1, 3)

Long-run gain

Now, we can translate the short-run gain to long-run gain for each metric and sales performance. To do this, we need to make use of the carryover (gamma) we get from responsiveness models (you may want to go back and check the tables named ‘table_responsiveness’). Again, for each mindset metric $M_i$ and sales:

$$ LRGain_{M_i} = Gain_{M_i}/(1-Carryover_{M_i}) $$
In [124]:
warnings.filterwarnings("ignore")

#Calculate long run gain for each mindset metric and sales
lrgain_awareness = round(gain_awareness / (1 - float(result_aware.params[1])), 3)
lrgain_consideration = round(gain_consideration / (1 - float(result_consideration.params[1])), 3)
lrgain_liking = round(gain_liking / (1 - float(result_liking.params[1])), 3)
lrgain_sales = round(gain_sales / (1 - float(result_sales.params[1])), 3)

Contribution

In the final step, we need to go back and refer to the conversion model (you may want to go back and check the table named ‘table_conversion’):

$$ Contribution_{M_i} = LRGain_{M_i} * Conversion_{M_i} $$
In [115]:
#Calculate conversion, i.e., decompose the gain and attribute to awareness, consideration, and liking

contribution_awareness = round(lrgain_awareness * table_final.loc[5, "awareness"], 3)
contribution_consideration = round(lrgain_consideration * table_final.loc[5, "consideration"], 3)
contribution_liking = round(lrgain_liking * table_final.loc[5, "liking"], 3)

Final Output

Finally, we are able to fill in all the empty cells:

In [116]:
table_whatif = pd.DataFrame({
    'Item': ["Advertising", "Promotion", "Awareness", "Consideration", "Liking", "Sales"],
    'Start': [round(val, 3) for val in [100, 100, np.mean(df['awareness']) / 100, np.mean(df['consideration']) / 100, np.mean(df['liking_transformed']), np.mean(df['vol'])]],
    'New': [round(val, 3) for val in [500, 100, new_awareness, new_consideration, new_liking, new_sales]],
    'Gain': [round(val, 3) for val in [400, 0, gain_awareness, gain_consideration, gain_liking, gain_sales]],
    'LRGain': ["/", "/", lrgain_awareness, round(lrgain_consideration, 3), round(lrgain_liking, 3), round(lrgain_sales, 3)],
    'Contribution': ["/", "/", round(contribution_awareness, 3), round(contribution_consideration, 3), round(contribution_liking, 3), "/"]
})

# Print the DataFrame
print(table_whatif)
            Item     Start       New     Gain LRGain Contribution
0    Advertising   100.000   500.000  400.000      /            /
1      Promotion   100.000   100.000    0.000      /            /
2      Awareness     0.275     0.299    0.088  0.092        0.074
3  Consideration     0.175     0.180    0.030  0.033        0.014
4         Liking     0.737     0.739    0.003  0.003        0.008
5          Sales  1800.391  1981.787    0.101  0.108            /

By summing up $Contribution_{awareness}$, $Contribution_{consideration}$, and $Contribution_{liking}$, we can get the total long run gain in sales that originates from improvement in mindset metrics.

  • We have: 0.074+0.014+0.008 = 0.096

We observe that:

  • Long term sales gain = 10.8%

  • Sales gain due to mindset metrics = 9.6%

  • Sales gain due to transactions = 10.8% - 9.6% = 1.2%

Therefore, we can tell that by quintupling advertising, there is a significant improvement in mindset metrics, and such improvement can contribute to a 8.6 per cent increase in sales. The eventual net effect on long-run sales is a 10.8 per cent increase, and this is due to a 1.2 per cent increase in sales due to transactions.

Next, we will examine the scenario in which the brand manager of Zanten, Zhang, doubles the promotion spending.

Doubling promotion spending

Suppose that the marketing manager of Zanten doubles the promotion spending from \£100K to \£200K and does not make further changes in the marketing mix. What would be the impact on long-term sales?

Let us visualise the calculations that we need to make by using the table below:

In [117]:
table_whatif2 = pd.DataFrame({
    'Item': ["Advertising", "Promotion", "Awareness", "Consideration", "Liking", "Sales"],
    'Start': [100, 100, round(np.mean(df['awareness']) / 100, 3), round(np.mean(df['consideration']) / 100, 3), round(np.mean(df['liking']) / 7, 3), round(np.mean(df['vol']), 3)],
    'New': [100, 200, "?", "?", "?", "?"],
    'Gain': [0, 100, "?", "?", "?", "?"],
    'LRGain': ["/", "/", "?", "?", "?", "?"],
    'Contribution': ["/", "/", "?", "?", "?", "/"]
})

# Print the DataFrame
print(table_whatif2)
            Item     Start  New Gain LRGain Contribution
0    Advertising   100.000  100    0      /            /
1      Promotion   100.000  200  100      /            /
2      Awareness     0.275    ?    ?      ?            ?
3  Consideration     0.175    ?    ?      ?            ?
4         Liking     0.737    ?    ?      ?            ?
5          Sales  1800.391    ?    ?      ?            /

The cells with question marks are the ones we need to fill in. We proceed as follows:

  1. Calculate the new level of each mindset metric and sales based on the changes that we decide to make to the marketing mix (i.e. doubling promotion spending).

  2. Then, by comparing the new and the old levels, we will calculate the short-run gain for each mindset metric and sales.

  3. Next, with the help of carryover parameters from responsiveness models, we will transform those short-run gains into corresponding long-run gains.

  4. Finally, we will decompose the long-run sales gain into transaction and mindset routes.

New

The column 'Start' from the above table summarizes the base values for each metric. For example, the average level of awareness, expressed in percentage terms, is 21.51%, leaving a 78.49% potential.

Next, we will see what happens to all the mindset metrics and sales after we quintuple advertising. To this end, we will determine the new level of each metric and sales after the change to the marketing mix variables (i.e.promotion). We need to use the estimated parameters from the responsiveness models. For mindset metric $M_i$ (i=1,2,3 in our case), we apply the formula below:

$$ New_{M_i} = Start_{M_i} *(Promotion_{new}/Promotion_{old})^{Responsiveness_{Promo_{M_i}}}) $$
In [118]:
table_whatif2['Start'] = pd.to_numeric(table_whatif2['Start'], errors='coerce')
table_whatif2['New'] = pd.to_numeric(table_whatif2['New'], errors='coerce')
table_whatif2['Gain'] = pd.to_numeric(table_whatif2['Gain'], errors='coerce')
table_whatif2['LRGain'] = pd.to_numeric(table_whatif2['LRGain'], errors='coerce')
table_whatif2['Contribution'] = pd.to_numeric(table_whatif2['Contribution'], errors='coerce')
In [119]:
#Note that we do not include short run impact of change in ads on each mindset metric and sales because there is no change (100/100=1)

new_awareness2 = round((1 - table_potential.iloc[0, 1]) * (table_whatif2.loc[1, 'New'] / table_whatif2.loc[1, 'Start']) ** table_final.loc[4, 'awareness'], 3)
new_consideration2 = round((1 - table_potential.iloc[1, 1]) * (table_whatif2.loc[1, 'New'] / table_whatif2.loc[1, 'Start']) ** table_final.loc[4, 'consideration'], 3)
new_liking2 = round((1 - table_potential.iloc[2, 1]) * (table_whatif2.loc[1, 'New'] / table_whatif2.loc[1, 'Start']) ** table_final.loc[4, 'liking'], 3)

# Calculate new sales
new_sales2 = round(np.mean(df['vol']) * (table_whatif2.loc[1, 'New'] / table_whatif2.loc[1, 'Start']) ** float(result_sales.params[3]), 3)

#### Short-run gain

In [120]:
# Calculate gain (from "start" to "new")
gain_awareness2 = round((new_awareness2 / (1 - table_potential.iloc[0, 1])) - 1, 3)
gain_consideration2 = round((new_consideration2 / (1 - table_potential.iloc[1, 1])) - 1, 3)
gain_liking2 = round((new_liking2 / (1 - table_potential.iloc[2, 1])) - 1, 3)

# Assuming result_conversion is the result of your regression
gain_sales2 = round((new_sales2 / np.mean(df['vol'])) - 1, 3)

Long-run gain

We can translate the short run gain into long run gain for each mindset metric and sales. To do this, we need to make use of the stickiness (carryover) parameters we get from the responsiveness models. You may want to go back and check the table named 'table_responsiveness'. So, for each metric ($M_i$), we calculate the long-run gain as follows:

$$ LRGain_{M_i} = Gain_{M_i}/(1-Carryover_{M_i}) $$
In [121]:
warnings.filterwarnings("ignore")

# Calculate long run gain for each mindset metric and sales
lrgain_awareness2 = round(gain_awareness2 / (1 - float(result_aware.params[1])), 3)
lrgain_consideration2 = round(gain_consideration2 / (1 - float(result_consideration.params[1])), 3)
lrgain_liking2 = round(gain_liking2 / (1 - float(result_liking.params[1])), 3)

# Assuming result_conversion is the result of your sales regression
lrgain_sales2 = round(gain_sales2 / (1 - float(result_conversion.params[1])), 3)

Contribution

In the final step, we refer to the conversion model. You may want to go back and check the table named 'table_conversion'. To find the contribution, we multiply the long run gain by the conversion parameter:

$$ Contribution_{M_i} = LRGain_{M_i} * Conversion_{M_i} $$
In [122]:
# Calculate conversion, i.e., decompose the gain and attribute to awareness, consideration, and liking
contribution_awareness2 = round(lrgain_awareness2 * table_final.loc[5, "awareness"], 3)
contribution_consideration2 = round(lrgain_consideration2 * table_final.loc[5, "consideration"], 3)
contribution_liking2 = round(lrgain_liking2 * table_final.loc[5, "liking"], 3)

Final output

Finally, we are able to fill in all the blank cells:

In [123]:
table_whatif2 = pd.DataFrame({
    'Item': ["Advertising", "Promotion", "Awareness", "Consideration", "Liking", "Sales"],
    'Start': [round(x, 3) for x in [100, 100, np.mean(df['awareness']) / 100, np.mean(df['consideration']) / 100, np.mean(df['liking_transformed']) / 7, np.mean(df['vol'])]],
    'New': [round(x, 3) for x in [100, 200, new_awareness2, new_consideration2, new_liking2, new_sales2]],
    'Gain': [round(x, 3) for x in [0, 100, gain_awareness2, gain_consideration2, gain_liking2, gain_sales2]],
    'LRGain': ["/", "/", lrgain_awareness2, round(lrgain_consideration2, 3), round(lrgain_liking2, 3), round(lrgain_sales2, 3)],
    'Contribution': ["/", "/", round(contribution_awareness2, 3), round(contribution_consideration2, 3), round(contribution_liking2, 3), "/"]
})

# Print the DataFrame
print(table_whatif2)
            Item     Start       New     Gain LRGain Contribution
0    Advertising   100.000   100.000    0.000      /            /
1      Promotion   100.000   200.000  100.000      /            /
2      Awareness     0.275     0.279    0.016  0.017        0.014
3  Consideration     0.175     0.183    0.047  0.052        0.022
4         Liking     0.105     0.747    0.014  0.014        0.039
5          Sales  1800.391  2211.931    0.229  0.258            /

By summing up $Contribution_{awareness}$, $Contribution_{consideration}$, and $Contribution_{liking}$, we can get the total long run gain in sales that originates from improvement in mindset metrics.

  • We have: 0.014 + 0.022 + 0.039 = 0.075

Given that there is a 25.8% gain in sales, we can tell that:

  • Long term sales gain = 25.8%
  • Sales gain due to mindset metrics = 7.5%
  • Sales gain due to transactions = 25.8%-7.5% = 18.3%

Recall that quintupling advertising expense resulted in a 10.8% increase in brand sales. The above analysis shows that doubling promotion results in a 25.8% increase in sales.Therefore, we can conclude that doubling promotion seems to be more effective in driving up sales. However, doubling promotion generates less improvement in consumer mindset metrics. If the ultimate objective of the marketing manager is to 'build a brand', then gains in mindset metrics should be more important. Therefore, depending on the strategic objective of the brand, Zhang, the marketing manager of Zanten, may want to choose either of these two strategies.

To summarize, marketing managers can conduct such scenario analyses, compare the estimated long-run effects, and make evidence-based decisions to win their customers' hearts and minds and boost sales.

5. Conclusion

In this tutorial, we discuss the role of customer mindset metrics – awareness, consideration, and liking – in guiding marketing mix decisions. These metrics are important intermediary performance indicators and allow brand managers to establish an actionable connection between marketing activity and financial performance. We conclude that the relative importance of customer mindset metrics is determined by four criteria: potential, stickiness, responsiveness, and conversion. Using these criteria and harnessing the data on customer mindset metrics, brand managers can assess the investment appeal of their marketing mix decisions and make evidence-based decisions.

Our case study on a fast-moving consumer goods brand, along with its Python application, demonstrates how brand managers can use analytic tools to develop actionable guidelines. These tools help improve marketing mix decisions by leveraging the diagnostic value of customer mindset metrics.